﻿using System;
using System.Collections.Generic;
using System.Linq;
using CardPlay.Model.ActionModel;
using CardPlay.Model.CardModel;
using CardPlay.Model.GameModel;
using CardPlay.Model.GameModel.Actions;
using CardPlay.Model.GameModel.Presentation;
using CardPlay.Model.Layout;
using CardPlay.Model.Primitives;
using Action = CardPlay.Model.GameModel.Actions.Action;

namespace CardPlay.DemoPack.Games.PyramidVariants
{
    [Game("5C3EE35E-3938-40B4-8DE0-EC4368259DD1", LevelOfDifficulty.Easy, LevelOfSkill.MostlyChance, DurationOfPlay.Within5To10Minutes, Family = FamilyName)]
    public class Pyramid : StandardCardGame
    {
        public const string FamilyName = "Pyramid";

        protected class CardSite
        {
            public Card Card { get; set; }
            public Size Offset { get; set; }
        }

        protected readonly Dictionary<string, TriangularArray<CardSite>> Layout = new Dictionary<string, TriangularArray<CardSite>>();

        public Pyramid()
        {
            NamesOfAdditionalPiles.AddRange(new[] { Reserve, Stock, Waste });

            CardsPerDeal = 1;
            MaximumDeals = 3;
            MaximumSelections = 2;

            PyramidDepth = 7;
            TotalFoundations = 0;
            TotalTableaus = 0;

            InitialDealCompleted += OnDealCompleted;
        }

        protected int PyramidDepth { get; set; }

        protected int MaximumSelections { get; set; }

        public override ILayoutSite CreatePresentation(IPresentationContext presentation)
        {
            ArrangePyramid(Layout[Reserve], presentation.Metrics);

            var root = presentation.CreateRowLayout();

            var stockWaste = presentation.CreateStockWasteLayout(this);
            root.Add(stockWaste);

            stockWaste.Direction = SpreadDirection.Vertical;

            var reserveStyle = new VisualTraits
            {
                Arrangement = StackPyramid,
                HorizontalAlignment = HorizontalAlignment.Center,
                ContentFit = ContentFit.FitBounds,
                MinimumSize = new Size(
                    PyramidDepth * (presentation.Metrics.CardSize.Width + presentation.Metrics.MinSpacingAcross), 
                    PyramidDepth * (presentation.Metrics.CardSize.Height / 2))
            };

            root.Add(presentation.CreateMimimalSpacer());
            root.Add(presentation.CreatePileVisual(Reserve, reserveStyle));

            return root;
        }

        public override int PercentComplete
        {
            get
            {
                var initialCount = CardsAtStart;
                var currentCount = CardPiles.Aggregate(0, (total, pile) => total + pile.Count);

                return 100 - (100 * currentCount) / initialCount;
            }
        }

        protected override bool IsComplete
        {
            get { return CardPiles.All(pile => pile.IsEmpty); }
        }

        protected override void PrepareDealScript(DealScript script)
        {
            var count = PyramidDepth * (PyramidDepth + 1) / 2;

            script.Deal(count + " up").To(Reserve);
            script.Deal((CardsInInitialDeal.Count() - count - 1) + " down;1 up").To(Stock);
        }

        protected override void OverridePlayingRules(PlayingRules playingRules)
        {
            base.OverridePlayingRules(playingRules);
            playingRules.Permit<RemovePileAction>().Never();
            playingRules.At(Stock).Permit<DealAction>().When(Action.IsActingOnFaceUpTopCard);
        }

        protected override bool IsChangeSelectionAllowedAt(PartLocation location)
        {
            if (Selection.Contains(location)) return true;

            var card = CardAt(location);

            return
                card != null && card.IsFaceUp &&
                (   (Selection.Count == 0 && ! location.Matches(Stock)) ||
                    (Selection.Count > 0 && Selection.Count < MaximumSelections && IsSelectionDiscardable(Selection.Concat(new[] { location })))) &&
                IsExposedLocation(location);
        }

        protected override IAction QueryActionAt(PartLocation location)
        {
            return location.Matches(Stock) ? base.QueryActionAt(location) : null;
        }

        protected override void OnTransactionEnding()
        {
            base.OnTransactionEnding();
            this.FlipFaceDownTopCardsInPilesMatching(Stock);
        }

        public override bool ExecuteCommandAt(PartLocation location, bool auto)
        {
            if (Selection.Contains(location))
            {
                Selection.Remove(location);
                return true;
            }

            if (IsChangeSelectionAllowedAt(location))
            {
                Selection.Add(location);

                if (IsSelectionDiscardable(Selection))
                {
                    Execute(new ComplexAction(
                        Selection
                            .OrderByDescending(l => l.Index)
                            .Select(selectedLocation => new RemoveCardAction(this, selectedLocation))));
                    Selection.Clear();
                }

                return true;
            }

            Selection.Clear();
            return base.ExecuteCommandAt(location, auto);
        }

        protected virtual bool IsSelectionDiscardable(IEnumerable<PartLocation> selection)
        {
            return SumOfRanks(selection.Select(CardAt)) == CardFace.RanksPerSuit;
        }

        protected virtual bool IsExposedLocation(PartLocation location)
        {
            return location.Matches(Waste) || location.Matches(Stock)
                ? location == PileAt(location).Top
                : Layout.ContainsKey(location.Path) && IsExposedPyramidLocation(location);
        }

        protected virtual bool IsExposedPyramidLocation(PartLocation location)
        {
            return !CoveringCardsAt(location).Any();
        }

        protected IEnumerable<PartLocation> CoveringCardsAt(PartLocation location)
        {
            var pile = PileAt(location);

            if (pile == null || pile.IsEmpty || !Layout.ContainsKey(location.Path))
                return new PartLocation[] { };

            var layout = Layout[location.Path];
            var pyramidIndex = layout.IndexWhere(item => item.Card == pile.CardAt(location.Index));

            return
                from cardInPile in pile
                join cardInLayout in layout.ChildIndicesOf(pyramidIndex).Select(n => layout[n].Card) on cardInPile equals cardInLayout
                select pile.LocationOf(cardInPile);
        }

        private void OnDealCompleted(object sender, EventArgs e)
        {
            var layout = new TriangularArray<CardSite>(PyramidDepth);

            for (var i = 0; i < this[Reserve].Count; ++i)
                layout[i] = new CardSite { Card = this[Reserve].CardAt(i) };

            Layout[Reserve] = layout;
        }

        private Size[] StackPyramid(CardPile pile, LayoutMetrics metrics)
        {
            if (String.IsNullOrEmpty(pile.Name)) return new Size[pile.Count];

            var l =
                (from layout in Layout[pile.Name]
                 join card in pile on layout.Card equals card
                 select layout.Offset).ToArray();

            return l;
        }

        private static void ArrangePyramid(TriangularArray<CardSite> layout, LayoutMetrics metrics)
        {
            var midPoint = metrics.CardSize.Width / 2;

            for (var row = 1; row < layout.Height; ++row)
            {
                var rowWidth = metrics.CardSize.Width * (row + 1) + metrics.MinSpacingAcross * row;
                var x = midPoint - rowWidth / 2;

                for (var column = 0; column <= row; ++column)
                {
                    layout[row, column].Offset = new Size(x, row * metrics.CardSize.Height / 2);
                    x += metrics.MinSpacingAcross + metrics.CardSize.Width;
                }
            }
        }

        protected static int SumOfRanks(IEnumerable<Card> setOfCards)
        {
            return setOfCards.Aggregate(0, (result, card) => result + (int)card.Rank);
        }
    }
}
